package org.hawk.gwt.ppc.reflect;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hawk.gwt.ppc.javacc.JavaParserTreeConstants;
import org.hawk.gwt.ppc.javacc.ParseException;
import org.hawk.gwt.ppc.scanner.Processor;
import org.hawk.gwt.ppc.scanner.SourcesLoader;
import org.hawk.gwt.ppc.scanner.Token;
import org.hawk.gwt.ppc.scanner.TokenChain;
import org.hawk.gwt.ppc.scanner.utils.JavaClassDescriptor;
import org.hawk.gwt.ppc.scanner.utils.JavaClassDescriptorUtils;
import org.hawk.gwt.ppc.scanner.utils.JavaTypeScope;
import org.hawk.gwt.ppc.utils.Name;
import org.hawk.gwt.ppc.utils.UniqueList;
/**
 * Collects all the metadata for classes and links it to the entry point of
 * the GWT module. If module has more than one entry point - all classes will be 
 * linked to the first one.
 * @author alex.bereznevatiy@gmail.com
 */
final class MetadataProcessor implements Processor {
	private static final int DEFAULT_STATE = 0;
	private static final int ALREADY_GOT_MAIN_CLASS = 1;
	private static final String IMPORT_KEYWORD = "import";
	
	private int state;
	private String currentPackage;
	private int currentPackageStart;
	private String currentClass;
	private List<String> processedClasses;
	private Map<String,List<String>> imports;
	private Map<String,String> importsCache;
	private Set<String> thirdPartyClasses;
	private List<String> interfaces;
	private SourcesLoader loader; 
	private StringBuilder output;
	/**
	 * Creates processor for package-internal use.
	 */
	MetadataProcessor(SourcesLoader loader){
		clear();
		this.loader = loader;
	}
	/**
	 * Clears all data for the current MetadataProcessor.
	 * Processor is still able to proceed with processing java sources but
	 * all previously stored metadata will become unavailable.
	 */
	void clear() {
		state = DEFAULT_STATE;
		imports = new HashMap<String,List<String>>();
		importsCache = new HashMap<String, String>();
		processedClasses = new LinkedList<String>();
		thirdPartyClasses = new HashSet<String>();
		interfaces = new UniqueList<String>();
		output = new StringBuilder();
	}
	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.Processor#visitStart(org.hawk.gwt.ppc.scanner.TokenChain, int)
	 */
	public void visitStart(TokenChain<? extends Token> iterator,int kind) {
		if (kind == JavaParserTreeConstants.JJTCOMPILATIONUNIT) {
			state = DEFAULT_STATE;
			currentPackage = "";
			currentClass = null;
		} else if (kind == JavaParserTreeConstants.JJTPACKAGEDECLARATION) {
			currentPackageStart = iterator.current().getIndex();
		} else if (kind == JavaParserTreeConstants.JJTCLASSORINTERFACEBODY || 
				kind == JavaParserTreeConstants.JJTEXTENDSLIST ||
				kind == JavaParserTreeConstants.JJTIMPLEMENTSLIST) {
			addClassIfNotGot(iterator);
		} else if (kind == JavaParserTreeConstants.JJTANNOTATIONTYPEBODY) {
			iterator.next();
			addClassIfNotGot(iterator);
		} else if (kind == JavaParserTreeConstants.JJTENUMBODY) {
			addClassIfNotGot(iterator);
		}
	}
	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.scanner.Processor#visitEnd(org.hawk.gwt.ppc.scanner.TokenChain, int)
	 */
	public void visitEnd(TokenChain<? extends Token> iterator,int kind) throws ParseException, IOException{
		if (kind == JavaParserTreeConstants.JJTCOMPILATIONUNIT) {
			importsCache.clear();
			buildClass(iterator);
		} else if (kind == JavaParserTreeConstants.JJTPACKAGEDECLARATION) {
			resolvePackageName(iterator);
		} else if (kind == JavaParserTreeConstants.JJTIMPORTDECLARATION) {
			addImportDeclaration(iterator);
		}
	}
	
	private List<Integer> collectInterfacesIndices(JavaTypeScope type, List<Integer> result) throws ParseException, IOException {
		if (type == null) {
			return result;
		}
		for (JavaTypeScope iface : type.getImplementedInterfaces()) {
			String name = iface.getResolvedClassName();
			interfaces.add(name);
			result.add(interfaces.indexOf(name));

			collectInterfacesIndices(iface, result);
		}
		collectInterfacesIndices(type.getSuperClass(), result);
		return result;
	}
	
	private void buildClass(TokenChain<? extends Token> iterator) throws ParseException, IOException {
		JavaClassDescriptor descriptor = JavaClassDescriptorUtils.getClassDescriptor(currentClass, loader);
		ClassDelegateBuilder builder = ClassDelegateBuilder.build(descriptor, loader);
		builder.setInterfaces(collectInterfacesIndices(descriptor.getCompilationUnit(), new LinkedList<Integer>()));
		
		if (builder.shouldBuildToClassSource()) {
			StringBuilder result = new StringBuilder(100);
			builder.write(result);
			thirdPartyClasses.addAll(builder.getThirdPartyClasses());
			result.append(iterator.current().getRawToken());
			iterator.current().setToken(result.toString());
			processedClasses.add(currentClass);
		} else {
			if (output.length() > 0) {
				output.append(',');
			}
			builder.write(output);
		}
	}
	
	private void resolvePackageName(TokenChain<? extends Token> iterator) {
		StringBuilder pkg = new StringBuilder();
		while(iterator.previous().getIndex()>
			currentPackageStart);
		while(!iterator.next().getToken().equals(";")){
			pkg.append(iterator.current().getToken());
		}
		currentPackage = pkg.toString();
	}

	private void addClassIfNotGot(TokenChain<? extends Token> iterator) {
		if (state == ALREADY_GOT_MAIN_CLASS) {
			return;
		}
		state = ALREADY_GOT_MAIN_CLASS;
		if (iterator.current().getToken().equals(">")) {
			skipGenericDeclaration(iterator);
		}
		currentClass = Name.getChildJavaName(currentPackage, iterator.current().getToken());
		if (imports.get(currentClass) == null) {
			imports.put(currentClass, new LinkedList<String>());
		}
		imports.get(currentClass).addAll(importsCache.values());
	}
	
	private void skipGenericDeclaration(TokenChain<? extends Token> iterator) {
		int deep = 1;
		while(deep > 0) {
			String tok = iterator.previous().getToken();
			if (tok.equals(">")) {
				deep++;
			} else if (tok.equals("<")) {
				deep--;
			}
		}
		iterator.previous();
	}
	private void addImportDeclaration(TokenChain<? extends Token> iterator) {
		StringBuilder value = new StringBuilder();
		while(!iterator.previous().getToken().equals(IMPORT_KEYWORD));
		while(!iterator.next().getToken().equals(";")){
			value.append(iterator.current().getToken());
		}
		String cls = value.toString();
		importsCache.put(Name.getSimpleName(cls), cls);
	}
	
	/**
	 * @return list of all collected top-level classes.
	 */
	public List<String> getTopClasses() {
		return Collections.unmodifiableList(processedClasses);
	}
	
	/**
	 * @param name
	 * @return list of all imported entries for the class with name
	 * specified.
	 */
	public List<String> getImports(String className) {
		List<String> result = imports.get(className);
		if(result==null){
			return Collections.emptyList();
		}
		return Collections.unmodifiableList(result);
	}
	
	/**
	 * @return name of the class currently processed or <code>null</code> if this name is not determined yet.
	 */
	public String getClassName() {
		return currentClass;
	}
	
	/**
	 * Metadata output to be flushed to some other place (not class body).
	 * E.g. in the entry point of the module.
	 * @return metadata array definition.
	 */
	public String getOutputForFlushing() {
		return output.toString();
	}
	
	/**
	 * @return classes that can be found by {@link Class#forName(String)}), but not allowed to be reflected.
	 */
	public Set<String> getThirdPartyClasses() {
		return Collections.unmodifiableSet(thirdPartyClasses);
	}
	
	/**
	 * @return list of interfaces for linking with classloader.
	 */
	public List<String> getInterfaces() {
		return Collections.unmodifiableList(interfaces);
	}
}
